package fr.uvsq.isty.bdh.hchess.engine;

import fr.uvsq.isty.bdh.hchess.gui.MainWindow;
import fr.uvsq.isty.bdh.hchess.gui.game.BoardDisplay;
import fr.uvsq.isty.bdh.hchess.gui.game.GameDisplay;
import fr.uvsq.isty.bdh.hchess.gui.menu.LoadingMenu;
import fr.uvsq.isty.bdh.hchess.gui.menu.SoloMenu;
import fr.uvsq.isty.bdh.hchess.interfacing.*;
import fr.uvsq.isty.bdh.hchess.ranking.RatingCalculator;
import ictk.boardgame.AmbiguousMoveException;
import ictk.boardgame.IllegalMoveException;
import ictk.boardgame.Move;
import ictk.boardgame.OutOfTurnException;
import ictk.boardgame.chess.*;
import ictk.boardgame.chess.io.PGNReader;
import ictk.boardgame.chess.io.PGNWriter;
import ictk.boardgame.io.InvalidGameFormatException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.Observable;
import java.util.Observer;
import javax.swing.Timer;

/**
 * Contain all the data used by the display.
 * @author Rémi Dubois <RemiEric.Dubois@gmail.com>
 */
public class GameData extends Observable {
    
    
    private ChessGame game; /** ChessGame (provided by ICTK) */
    private ChessBoard board; /** ChessBoard (provided by ICTK) */
    
    private MainWindow mainWindow;
    
    private IChessPlayer whitePlayer;
    private IChessPlayer blackPlayer;
    
    private Square pickedPieceOrigin;
    private int pickedNewPosX;
    private int pickedNewPosY;
    
    private ChessPiece pickedPiece;
    private ChessMove lastMove;
    
    private PipedReader communicationPipe;
    private Timer refereePipeUpdater;
    private Referee referee;
    private boolean refereePipeExiting;
    
    private BoardDisplay boardDisplay;
    
    private boolean gameOver;
    private GameEndResult result;
    
    
    private GameState currentGameState;
    
    private int promotion;
    private boolean promoting;
    private final Object promotionLock = new Object();
    private ChessPiece promotedPiece;
    private Square promotedDestination;
    
    private boolean helpEnable;
    
    public GameData(MainWindow mw) {
        mainWindow = mw;
        boardDisplay = null;
        
        
        whitePlayer = null;
        blackPlayer = null;
        
        pickedPiece = null;
        lastMove = null;
        
        gameOver = false;
        promoting = false;
        
        communicationPipe = null;
        currentGameState = GameState.OUT;
        referee = null;
        refereePipeUpdater = null;
        refereePipeExiting = false;
        
        helpEnable = true;
    }
    /**
     * Starts a game with the two given players and a given state.
     * The interface is changed in consequence.
     * @param playerWhite the black player
     * @param playerBlack the white player
     * @param _game the game wich will be continued
     */
    synchronized public void startGame(IChessPlayer playerWhite, IChessPlayer playerBlack, ChessGame _game) {
        currentGameState = GameState.LOADING_GAME;
        
        //Loading screen
        mainWindow.setContentPane(new LoadingMenu());
        mainWindow.setVisible(true);
        mainWindow.repaint();
        communicationPipe = new PipedReader();
        if(_game == null) {
            try {
                referee = new Referee(playerWhite, playerBlack, new PipedWriter(communicationPipe));
            } catch (IOException ex) {
                assert false : "on va tous mourru";
            }
        }
        else {
            ChessBoard b;
            int[][] moves;
            moves = Helpers.intArrayFromCurrentHistoryPosition(_game.getHistory());
            // Rewind for getting the initial board.
            _game.getHistory().rewind();
            b = Helpers.copyBoard((ChessBoard)_game.getBoard());

            try {
                refereePipeExiting = false;
                referee = new Referee(playerWhite, playerBlack, new PipedWriter(communicationPipe), b, moves);
            } catch (IOException ex) {
                assert false : "on va tous mourru";
            }
        }
        
        boardDisplay = mainWindow.getGameDisplay().getBoardDisplay();
        this.whitePlayer = playerWhite;
        this.blackPlayer = playerBlack;
        
        if(playerWhite instanceof HumanPlayer){
            addWhiteHumanPlayer((HumanPlayer)playerWhite);
        }
        if(playerBlack instanceof HumanPlayer){
            addBlackHumanPlayer((HumanPlayer)playerBlack);
        }
        
        referee.start();
        
        int delay = 25; //milliseconds
        
        ActionListener boardUpdater = new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                watchPipe();
            }
        };
        refereePipeUpdater = new Timer(delay, boardUpdater);
        refereePipeUpdater.start();
    }
    
    /**
     * Watch if there are event in the pipe and extract the data that are send through the pipe.
     */
    private void watchPipe(){
        try {
            if(!refereePipeExiting){
                if(communicationPipe.ready()) {
                    int eventType = communicationPipe.read();
                    if(eventType == Referee.PIPE_MOVE && board!=null) {
                        char[] buffer = new char[Helpers.getMoveSizeFixedSerialized()];
                        communicationPipe.read(buffer, 0, Helpers.getMoveSizeFixedSerialized());

                        lastMove = Helpers.fixedSizeUnserializeMove(board, new String(buffer));
                        addMove(lastMove);
                    }
                    else if (eventType == Referee.PIPE_EVENT) {
                        GameEvent event = GameEvent.values()
                                [communicationPipe.read()];
                        if(event==GameEvent.REFEREE_EXITING){
                            refereePipeExiting = true;
                        }else if(event==GameEvent.REFEREE_STATE_RESTORED){
                            showBoard();
                        }else{
                            result = Helpers.getWinner(event);
                            gameOver = true;
                            
                            if(whitePlayer instanceof HumanPlayer && whitePlayer instanceof HumanPlayer){
                            	String whitePlayerName = ((HumanPlayer)whitePlayer).getName();
                            	String blackPlayerName = ((HumanPlayer)blackPlayer).getName();
                            	
                            	if(result == GameEndResult.BLACK_WIN) {
                                	RatingCalculator.updateRatingsWin(blackPlayerName, whitePlayerName);
                            	}
                            	else if (result == GameEndResult.WHITE_WIN){
                            		RatingCalculator.updateRatingsWin(whitePlayerName, blackPlayerName);
                            	}
                            	//draw
                            	else {
                            		RatingCalculator.updateRatingsDraw(whitePlayerName, blackPlayerName);
                            	}
                            	
                            }
                            
                            setChanged();
                            notifyObservers();
                        }
                    }else if(eventType == Referee.PIPE_BOARD){
                        initBoardFromPipe(communicationPipe);
                        registerViewListeners();
                    }
                }
            }
        } catch (IOException ex) {
            // // Hacky Hacky.
            // endGame();
            ex.printStackTrace();
            assert false : "Problems with the pipe.";
        }
    }
    
    /**
     * Starts a game with the two given players and a default initial state.
     * @param playerWhite the white player
     * @param playerBlack the black player
     */
    public void startGame(IChessPlayer playerWhite, IChessPlayer playerBlack) {
        startGame(playerWhite, playerBlack, null);
    }
    
    /**
     * If a game is currently running, it stops it more or less violently.
     * A game is considered running even if it is still loading.
     * It will close the players and (someday kill the referee).
     */
    synchronized public void endGame(){
        if(getCurrentGameState()!=GameState.OUT){
            if((getCurrentGameState()==GameState.LOADING_GAME
                    || getCurrentGameState()==GameState.IN_GAME)
                    && referee!= null){
                // Kill players and referee.
                if(whitePlayer instanceof IDisposableChessPlayer){
                    ((IDisposableChessPlayer)whitePlayer).close();
                }
                if(blackPlayer instanceof IDisposableChessPlayer){
                    ((IDisposableChessPlayer)blackPlayer).close();
                }
                // the referee should stop after this, by lack of players.
            }
            while(!refereePipeExiting){
                watchPipe();
            }
            
            if(getCurrentGameState()==GameState.IN_GAME){
                currentGameState = GameState.OUT;
                unregisterViewListeners();
            }
            refereePipeExiting = false;
            refereePipeUpdater.stop();
            boardDisplay = null;

            whitePlayer = null;
            blackPlayer = null;

            pickedPiece = null;
            lastMove = null;

            gameOver = false;
            promoting = false;

            communicationPipe = null;
            referee = null;
        }
    }
    
    /**
     * End a game or a replay.
     */
    public void endAnything() {
        if (currentGameState == GameState.IN_REPLAY) {
            endReplay();
        }
        else if (currentGameState == GameState.IN_GAME
                || currentGameState == GameState.LOADING_GAME) {
            endGame();
        }
    }
    
    /**
     * Start a replay.
     * @param gameToReplay the game to replay
     */
    synchronized public void startReplay(ChessGame gameToReplay){
        if(currentGameState == GameState.IN_GAME
                ||currentGameState == GameState.LOADING_GAME){
            endGame();
        }
        currentGameState = GameState.IN_REPLAY;
        game = gameToReplay;
        board = (ChessBoard) game.getBoard();
        
        registerViewListeners();
        //this.addObserver(mainWindow.getReplayToolBar());
        showBoardDisplay(true);
        
        setChanged();
        notifyObservers();
    }
    
    /**
     * End a running replay.
     */
    synchronized public void endReplay(){
        currentGameState = GameState.OUT;
        unregisterViewListeners();
        boardDisplay = null;

        whitePlayer = null;
        blackPlayer = null;

        pickedPiece = null;
        lastMove = null;

        gameOver = false;
        promoting = false;

        communicationPipe = null;
        referee = null;
    }
    
    /**
     * Set as observer of the GameData the main displays
     */
    private void registerViewListeners(){
        GameDisplay gameDisplay = mainWindow.getGameDisplay();
        this.addObserver(gameDisplay.getBoardDisplay());
        this.addObserver(gameDisplay.getBlackCapturedPiecesDisplay());
        this.addObserver(gameDisplay.getWhiteCapturedPiecesDisplay());
        this.addObserver(gameDisplay.getCurrentPlayerDisplay());
        this.addObserver(gameDisplay.getPromotionChoicePanel());
        this.addObserver(gameDisplay.getSecondaryDisplayContainer());
        this.addObserver(mainWindow.getItemSave());
        setChanged();
        this.notifyObservers();
    }
    
    /**
     * Remove most of the observers of the GameData
     */
    private void unregisterViewListeners(){
        setChanged();
        this.notifyObservers();
        GameDisplay gameDisplay = mainWindow.getGameDisplay();
        this.deleteObserver(gameDisplay.getBoardDisplay());
        this.deleteObserver(gameDisplay.getBlackCapturedPiecesDisplay());
        this.deleteObserver(gameDisplay.getWhiteCapturedPiecesDisplay());
        this.deleteObserver(gameDisplay.getCurrentPlayerDisplay());
        this.deleteObserver(gameDisplay.getPromotionChoicePanel());
        this.deleteObserver(gameDisplay.getSecondaryDisplayContainer());
        this.deleteObserver(mainWindow.getItemSave());
    }
    
    private void showBoard(){
        currentGameState = GameState.IN_GAME;
        showBoardDisplay(false);
    }
    
    /**
     * initialise the board getting it form the referee through the given pipe. 
     * @param boardInitilizationPipe the pipe from wich we initialise the board
     */
    private void initBoardFromPipe(PipedReader boardInitilizationPipe) {
        ChessBoard newBoard;
        char[] buffer = new char[Helpers.getBoardSizeFixedSerialized()];
        try {
            boardInitilizationPipe.read(buffer, 0, Helpers.getBoardSizeFixedSerialized());
        } catch (IOException ex) {
            assert false : "The pipe shall not fail.";
        }
        
        newBoard = Helpers.fixedSizeUnserializeBoard(new String(buffer));
        
        board = newBoard;
        this.game = new ChessGame(new ChessGameInfo(), board);
    }
    
    /**
     * display the board in the given mod (replay or game)
     * @param displayReplayToolbar true if in replay mode
     */
    private void showBoardDisplay(boolean displayReplayToolbar){
        if (displayReplayToolbar) {
            mainWindow.switchToReplayMode();
        } else {
            mainWindow.switchToGameMode();
        }
        
        mainWindow.repaint();
        
        this.setChanged();
        this.notifyObservers();
    }
    
    @Override
    public void addObserver(Observer o) {
        super.addObserver(o);
        
        if(o instanceof BoardDisplay) {
            boardDisplay = (BoardDisplay) o;
        }
    }
    
    /**
     * Connect the given HumanPlayer to the BoardDisplay as a white player 
     * allowing him to get the inputs he need to play its moves.
     * @param whitePlayer the human player to connect as a white player
     */
    private void addWhiteHumanPlayer(HumanPlayer whitePlayer) {
        whitePlayer.connectToDisplay(boardDisplay, false);
    }
    
    /**
     * Connect the given HumanPlayer to the BoardDisplay as a black player 
     * allowing him to get the inputs he need to play its moves.
     * @param blackPlayer the human player to connect as a black player
     */
    private void addBlackHumanPlayer(HumanPlayer blackPlayer) {
        blackPlayer.connectToDisplay(boardDisplay, true);
    }
    
    /**
     * Add a new move in the game.
     * @param move the move to add
     */
    private void addMove(ChessMove move) {
        try {
            game.getHistory().add(move);
        } catch (IndexOutOfBoundsException ex) {
        } catch (OutOfTurnException ex) {
        } catch (AmbiguousMoveException ex) {
        } catch (IllegalMoveException ex) {
        }
        
        this.setChanged();
        this.notifyObservers();
    }
    
    /**
     * @return true if it's the black player turn
     */
    public boolean isBlackTurnToPlay() {
        if (game.getPlayerToMove() == 1) {
            return true;
        }
        else {
            return false;
        }
    }
    
    /**
     * @return the whitePlayer
     */
    public IChessPlayer getWhitePlayer() {
        return whitePlayer;
    }

    /**
     * @return the blackPlayer
     */
    public IChessPlayer getBlackPlayer() {
        return blackPlayer;
    }
    
    /**
     * @return the current human player or null if the current player is not human.
     */
    public HumanPlayer getCurrentHumanPlayer() {
        if (this.isBlackTurnToPlay() && blackPlayer instanceof HumanPlayer) {
            return (HumanPlayer)blackPlayer;
        }
        else if((!this.isBlackTurnToPlay()) && whitePlayer instanceof HumanPlayer) {
            return (HumanPlayer)whitePlayer;
        }
        return null;
    }
    
    /**
     * @return a copy of the board
     */
    public ChessBoard getBoard() {
        return Helpers.copyBoard(board);
    }
    
    /**
     * Tell that a piece was picked and tell wich one and by wich player.
     * @param originFile the file in wich is the piece we pick
     * @param originRank the rank in wich is the piece we pick
     * @param pickedNewPosX the x-axis position in wich the piece will now be displayed
     * @param pickedNewPosY the y-axis position in wich the piece will now be displayed
     * @param player the player who pick the piece
     */
    public void setPick(int originFile, int originRank, int pickedNewPosX, int pickedNewPosY, HumanPlayer player) {
        
        pickedPieceOrigin = board.getSquare(originFile, originRank);
        
        ChessPiece pieceToPick = pickedPieceOrigin.getOccupant();
        
        if( pieceToPick != null && 
                ((player.isBlackPlayer() && pieceToPick.isBlack())
                || (!(player.isBlackPlayer()) && !(pieceToPick.isBlack())))) {
            
            this.pickedNewPosX = pickedNewPosX;
            this.pickedNewPosY = pickedNewPosY;
            this.pickedPiece = pieceToPick;
            
            this.setChanged();
            this.notifyObservers();
        }
        else {
            pickedPieceOrigin = null;
        }
    }
    
    /**
     * Drop the picked piece at a given position.
     * @param destinationFile the file in wich we drop the piece
     * @param destinationRank the rank in wich we drop the piece
     * @param player the player who drop the piece
     */
    public void releasePicked(int destinationFile, int destinationRank, HumanPlayer player) {
        if(pickedPiece != null) {
            
            try {
                if(checkMove(pickedPieceOrigin.getX(), pickedPieceOrigin.getY(), destinationFile, destinationRank)) {
                    player.setMove(pickedPieceOrigin.getX(), pickedPieceOrigin.getY(), destinationFile, destinationRank, false);
                }
                
                this.pickedPiece = null;

                this.setChanged();
                this.notifyObservers();
                
            } catch (MoveIsPromotionException ex) {
                
                this.promotedPiece = pickedPiece;
                this.promotedDestination = new Square((byte)destinationFile, (byte)destinationRank);
                this.pickedPiece = null;

                this.setChanged();
                this.notifyObservers();
                
                player.setMove(pickedPieceOrigin.getX(), pickedPieceOrigin.getY(), destinationFile, destinationRank, true);
            }
            
            
            
        }
    }
    
    /**
     * Set the new position of the picked piece. 
     * @param pickedNewPosX the new x-axis position of the piece
     * @param pickedNewPosY the new y-axis position of the piece
     */
    public void setPickedPos(int pickedNewPosX, int pickedNewPosY) {
        this.pickedNewPosX = pickedNewPosX;
        this.pickedNewPosY = pickedNewPosY;
        
        this.setChanged();
        this.notifyObservers();
    }

    /**
     * @return the x-axis position of the picked piece
     */
    public int getPickedNewPosX() {
        return pickedNewPosX;
    }

    /**
     * @return the y-axis position of the picked piece
     */
    public int getPickedNewPosY() {
        return pickedNewPosY;
    }
    
    /**
     * @return the pickedPiece
     */
    public ChessPiece getPickedPiece() {
        return pickedPiece;
    }
    
    /**
     * @return the coordinates of the last move
     */
    public int[] getLastMoveCoordinates() {
        if(lastMove != null) {
            if (lastMove.isCastleKingside()) {
                int []coordinates = new int[4];
                coordinates[0] = 5;
                coordinates[2] = 7;
                if(isBlackTurnToPlay()) {
                    coordinates[1] = 1;
                    coordinates[3] = 1;
                } 
                else {
                    coordinates[1] = 8;
                    coordinates[3] = 8;
                }
                return coordinates;
            }
            else if (lastMove.isCastleQueenside()) {
                int []coordinates = new int[4];
                coordinates[0] = 5;
                coordinates[2] = 3;
                if(isBlackTurnToPlay()) {
                    coordinates[1] = 1;
                    coordinates[3] = 1;
                } 
                else {
                    coordinates[1] = 8;
                    coordinates[3] = 8;
                }
                return coordinates;
            }
            else {
                return lastMove.getCoordinates();
            }
        }
        else {
            return null;
        }
    }
    
    /**
     * @return true if the game is over
     */
    public boolean isGameOver() {
        return gameOver;
    }

    /**
     * @return the result of the game
     */
    public GameEndResult getResult() {
        return result;
    }

    /**
     * @param getBlackPiece true if we want the black pieces that have been captured.
     * @return the black or white pieces that have been captured
     */
    public ChessPiece[] getCapturedPieces(boolean getBlackPiece) {
        return board.getCapturedPieces(getBlackPiece);
    }
    
    /**
     * Check if the given move is valid applied to the current board
     * @param originFile the file where is the piece we want to move
     * @param originRank the rank where is the piece we want to move
     * @param destinationFile the file where we want to move the piece 
     * @param destinationRank the rank where we want to move the piece 
     * @return true if the move is valid
     * @throws MoveIsPromotionException 
     */
    boolean checkMove(int originFile, int originRank, int destinationFile, int destinationRank) throws MoveIsPromotionException {
        if(originFile <= 8 && originFile > 0 &&
                originRank <= 8 && originRank > 0 &&
                destinationFile <= 8 && destinationFile > 0 &&
                destinationRank <= 8 && destinationRank > 0) {
            try {
                ChessMove move = new ChessMove(board, originFile, originRank, destinationFile, destinationRank);
                if(move.getPromotion() != null) {
                    throw new MoveIsPromotionException();
                }
                return true;
            } catch (IllegalMoveException ex) {
            }
        }
        
        return false;
    }
    
    /**
     * Set the promotion to be the given promotionand notify that the promotion 
     * have been chosen.
     * @param promotion the piece in wich we want to promote, can be Queen.INDEX, Rook.INDEX, Bishop.INDEX and Knight.INDEX
     */
    public void setPromotion(int promotion) {
        synchronized (promotionLock) {
            this.promotion = promotion;
            promotionLock.notify();
        }
    }
    
    /**
     * Get the promotion chosen by a player.
     * @return the piece in wich we want to promote, can be Queen.INDEX, Rook.INDEX, Bishop.INDEX and Knight.INDEX
     */
    public int getPromotion() {
        synchronized (promotionLock) {
            promotion = -1;
            promoting = true;
            
            this.setChanged();
            this.notifyObservers();
            
            try {
                promotionLock.wait();
            } catch (InterruptedException ex) {
            }
            
            promoting = false;
            pickedPiece = null;
            
            this.setChanged();
            this.notifyObservers();
            
            return promotion;
        }
    }

    /**
     * @return true if the game data is waiting to get a promotion
     */
    public boolean isPromoting() {
        return promoting;
    }
    
    //
    // Replay
    //
    /**
     * @return true if we are not at the end of the replay,
     * i.e. if we are not at the end of the played game.
     */
    public boolean replay_CanGoForward(){
        return game.getHistory().hasNext();
    }
    
    /**
     * @return false if we are at the starting position, and therefore cannot
     * go backward.
     */
    public boolean replay_canGoBackward(){
        return game.getHistory().getCurrentMove() != null;
    }
    /**
     * Go one move forward, or does nothing if already at the end.
     */
    public void replay_goOneForward(){
        if(replay_CanGoForward()){
            game.getHistory().next();
            setChanged();
            notifyObservers();
        }
    }
    
    /**
     * Go one move backward, or does nothing if we are at the start of the game.
     */
    public void replay_oneBackward(){
        if(replay_canGoBackward()){
            game.getHistory().prev();
            setChanged();
            notifyObservers();
        }
    }
    /**
     * Go to the begining of the game, before the first move is played.
     */
    public void replay_rewind(){
        game.getHistory().rewind();
        setChanged();
        notifyObservers();
    }
    
    /**
     * Go to the end of the game.
     * the game does not have to be over for this method to work, it just goes
     * to the last move played.
     */
    public void replay_goToEnd(){
        game.getHistory().goToEnd();
        setChanged();
        notifyObservers();
    }
    
    /**
     * @return true if the game was finished. Does not indicate if the current 
     * move finishes the game, but if in the end, the game was finished.
     */
    public boolean replay_isGameFinished(){
        return !game.getCurrentResult().isUndecided();
    }
    
    /**
     * Sends to the SoloMenu to choose players, and the game launched there will
     * be at the current replay position.
     */
    public void replay_switchToPlay(){
        endReplay();
        mainWindow.setContentPane(new SoloMenu(mainWindow,game));
        mainWindow.setVisible(true);
        mainWindow.repaint();
    }
    
    /**
     * Save the current game to a pgn format
     * @param out an OutputStream leading to where we want to save on pgn
     * @throws IOException 
     */
    public void saveCurrentPlayingToPGN(OutputStream out) throws IOException {
        PGNWriter writer;
        Move origin = game.getHistory().getCurrentMove();
        writer = new PGNWriter(out);
        writer.setIndentVariations(true);
        writer.setIndentComments(true);
        game.getHistory().rewind();
        writer.writeGame(game);
        if(origin!=null){
            game.getHistory().goTo(origin);
        }
    }
    
    /**
     * Read a pgn file and initialise a game from it content.
     * @param in a reader where we should read the pgn file
     * @return the game loaded from the reader
     * @throws InvalidGameFormatException
     * @throws IllegalMoveException
     * @throws AmbiguousMoveException
     * @throws IOException 
     */
    public static ChessGame loadGameFromPGN(Reader in) throws InvalidGameFormatException, IllegalMoveException, AmbiguousMoveException, IOException {
        ChessGame g;
        
        PGNReader pgn = new PGNReader(in);
        pgn.readGame();
        
        g = (ChessGame) pgn.getGame();
        if (g.getGameInfo() == null) {
            throw new IOException("Invalid PGN file.");
        }
        // End for getting all the history.
        g.getHistory().goToEnd();
        
        return g;
    }
    
    /**
     * @return the current game state
     */
    public GameState getCurrentGameState() {
        return currentGameState;
    }

    /**
     * @return the promoted piece
     */
    public ChessPiece getPromotedPiece() {
        return promotedPiece;
    }

    /**
     * @return the promoted piece destination
     */
    public Square getPromotedDestination() {
        return promotedDestination;
    }
    
    /**
     * Enable or disable the display of the possible moves
     * @param helpEnable true to activate the help false to disactivate it
     */
    public void enableHelp(boolean helpEnable) {
        this.helpEnable = helpEnable;
    }

    /**
     * @return true if the help is enable
     */
    public boolean isHelpEnable() {
        return helpEnable;
    }
    
}
